package org.gwt.grin.client;

import java.util.HashMap;
import java.util.Map;

import org.gwt.grin.client.animator.AnimationClient;
import org.gwt.grin.client.commands.ActivateSegmentCommand;
import org.gwt.grin.client.commands.Command;
import org.gwt.grin.client.commands.SegmentDoneCommand;
import org.gwt.grin.client.commands.SyncDisplayCommand;
import org.gwt.grin.client.input.RCHandler;
import org.gwt.grin.client.input.RCKeyEvent;
import org.gwt.grin.client.util.Graphics;
import org.gwt.grin.client.util.Queue;

import com.google.gwt.user.client.ui.Widget;

/**
 * Represents a show. A show is the top-level node in a scene graph. It is
 * composed of a number of segments. A show progresses by moving through
 * segments; a show has exactly one active segment while it is running. A
 * segment is composed of a number of visual features, TODO ...
 * 
 * @author Bill Foote (http://jovial.com)
 * @author ggeorg
 */
public class Show implements AnimationClient {

	private Director director;
	// Never null

	private Widget widget;

	private Segment currentSegment = null;

	private SyncDisplayCommand syncDisplayCommand = null;
	private SegmentDoneCommand segmentDoneCommand = null;

	// For push/pop
	private Segment[] segmentStack = new Segment[0];
	private int segmentStackPos = 0;
	private ActivateSegmentCommand popSegmentCommand = null;

	protected Segment[] segments;
	protected Feature[] features;
	protected RCHandler[] rcHandlers;

	protected final Map<String, Segment> publicSegments = new HashMap<String, Segment>();
	protected final Map<String, Feature> publicFeatures = new HashMap<String, Feature>();
	protected final Map<String, RCHandler> publicRCHandlers = new HashMap<String, RCHandler>();
	protected final Map<String, Command> publicNamedCommands = new HashMap<String, Command>();

	private boolean initialized = false;
	private boolean destroyed = false;

	private Queue pendingCommands = new Queue(32);
	private boolean deferringPendingCommands = false;

	public Show() {
		this(null);
	}

	public Show(Director director) {
		if (director == null) {
			director = new Director();
		}
		this.director = director;
		director.setShow(this);
	}

	/**
	 * This is called to build the show. This needs to be done before initialize
	 * is called. Normally, clients of the GRIN framework shouldn't call this.
	 * 
	 * @param segments
	 * @param features
	 * @param publicNamedCommands
	 */
	public void buildShow(Segment[] segments, Feature[] features,
			RCHandler[] rcHandlers, Map<String, Command> publicNamedCommands) {
		this.segments = segments;
		this.features = features;
		this.rcHandlers = rcHandlers;

		for (int i = 0; i < segments.length; i++) {
			Segment s = segments[i];
			if (s.getName() != null) {
				publicSegments.put(s.getName(), s);
			}
			s.setShow(this);
		}

		for (int i = 0; i < features.length; i++) {
			Feature f = features[i];
			if (f.getName() != null) {
				publicFeatures.put(f.getName(), f);
			}
			f.setShow(this);
		}

		for (int i = 0; i < rcHandlers.length; i++) {
			RCHandler h = rcHandlers[i];
			if (h.getName() != null) {
				publicRCHandlers.put(h.getName(), h);
			}
			h.setShow(this);
		}
	}

	/**
	 * Get this show's director. If the show was created without a director, a
	 * default one is created, and this will be returned.
	 * 
	 * @return our Director
	 */
	public Director getDirector() {
		return director;
	}

	public void initialize(Widget w) {
		assert !initialized : "Initialize called twice";

		initialized = true;
		this.widget = w;

		// TODO stickyImages

		popSegmentCommand = new ActivateSegmentCommand(this, false, true);
		// TODO SetupManager related things & start

		for (int i = 0; i < segments.length; i++) {
			segments[i].initialize();
		}
		for (int i = 0; i < features.length; i++) {
			features[i].initialize();
		}

		// TODO showTop
		// TODO initializer = null;
	}

	public void destroy() {
		assert initialized : "Destroy of uninitialized show";

		if (currentSegment != null) {
			currentSegment.deactivate();
			currentSegment = null;
		}

		// TODO showTop deactivate

		for (int i = 0; i < features.length; i++) {
			features[i].destroy();
		}

		// TODO stickyImages deactivate

		destroyed = true;
		// TODO setupManager.stop()
		director.notifyDestroyed();
	}

	/**
	 * Used to build the show; clients of GRIN framework should not call this
	 * method directly.
	 */
	public void setSegmentStackDepth(int depth) {
		segmentStack = new Segment[depth];
	}

	/**
	 * Look up the given public feature.
	 * 
	 * @param name
	 *            the feature name
	 * @return feature, or {@code null} if not found
	 */
	public Feature getFeature(String name) {
		return publicFeatures.get(name);
	}

	/**
	 * Look up the given public named command or command list. This Command
	 * object can be used in a call to runCommand(Command) on the show from
	 * where it came. If you send a named command to a different show, the
	 * results are undefined.
	 * 
	 * @return the Command, or {@code null}
	 **/
	public Command getNamedCommand(String name) {
		return publicNamedCommands.get(name);
	}

	/**
	 * Get a public RC handler by name.
	 * 
	 * @param name
	 *            the RC handler's name
	 * @return RC handler, or {@code null} if not found
	 */
	public RCHandler getRCHandler(String name) {
		return publicRCHandlers.get(name);
	}

	/**
	 * Look up a public segment. It's OK to call this before the show is
	 * initialized.
	 * 
	 * @param name
	 *            the segment name
	 * @return segment, or {@code null} if not found
	 */
	public Segment getSegment(String name) {
		return publicSegments.get(name);
	}

	/**
	 * Get the depth of the segment stack. This is how many times you can push a
	 * segment without an old value falling off the end of the stack. It's set
	 * in the show file.
	 */
	public int getSegmentStackDepth() {
		return segmentStack.length;
	}

	/**
	 * Set the current segment. This is the main way an application controls
	 * what is being displayed on the screen. The new segment will become
	 * current when we advance to the next frame.
	 * <p>
	 * If the show has been destroyed, calling this method has no effect. It's
	 * OK to call this before the show has been initialized.
	 * <p>
	 * The current segment is not pushed onto the segment activation stack.
	 * 
	 * @param seg
	 *            the segment to activate, or {@code null} to pop the segment
	 *            activation stack
	 */
	public void activateSegment(Segment seg) {
		activateSegment(seg, false);
	}

	/**
	 * Set the current segment. This is the main way an application controls
	 * what is being displayed on the screen. The new segment will become
	 * current when we advance to the next frame.
	 * <p>
	 * If the show has been destroyed, calling this method has no effect. It's
	 * OK to call this before the show has been initialized.
	 * 
	 * @param seg
	 *            the segment to activate, or {@code null} to pop the segment
	 *            activation stack
	 * @param push
	 *            when {@code true} and when the segment is non-{@code null},
	 *            the current segment will be pushed onto the segment activation
	 *            stack as the show transitions to the new segment.
	 */
	public void activateSegment(Segment seg, boolean push) {
		assert seg != null;
		assert seg.show == this;

		if (seg == null) {
			runCommand(popSegmentCommand);
		} else {
			assert (seg.show == this);
			runCommand(seg.getCommandToActivate(push));
		}
	}

	/**
	 * Used by the activate segment command. Clients of the GRIN framework
	 * should never call this method directly.
	 */
	public synchronized void pushCurrentSegment() {
		segmentStack[segmentStackPos] = currentSegment;
		segmentStackPos = (segmentStackPos + 1) % segmentStack.length;
	}

	/**
	 * Used by the activate segment command. Clients of the GRIN framework
	 * should never call this method directly.
	 */
	public synchronized Segment popSegmentStack() {
		segmentStackPos--;
		if (segmentStackPos < 0) {
			segmentStackPos = segmentStack.length - 1;
		}
		Segment result = segmentStack[segmentStackPos];
		segmentStack[segmentStackPos] = null;
		return result;
	}

	/**
	 * Synchronize the display to the current show state. This works by queuing
	 * a show command. When executed, this command will prevent the execution of
	 * any other show commands, until the display has caught up to the internal
	 * program state. This is a good thing to do before a command that is
	 * potentially time-consuming.
	 * <p>
	 * Note carefully the difference between {@link #syncDisplay()} and
	 * {@link #deferNextCommands()}. If your're about to queue a command that
	 * does something time-consuming, then you should call
	 * {@link #syncDisplay()} first, so the display is guaranteed to be updated.
	 * If, however, you're executing within the body of the command and you want
	 * to make sure the display gets synchronized before the next command in the
	 * queue (which was already there), then you want to call
	 * {@link #deferNextCommands()}.
	 * <p>
	 * This is equivalent to the {@code sync_display} GRIN command.
	 */
	public void syncDisplay() {
		if (syncDisplayCommand == null) {
			syncDisplayCommand = new SyncDisplayCommand(this);
		}
		runCommand(syncDisplayCommand);
	}

	/**
	 * Signal to the show that the current segment is done. This causes the
	 * segment to execute its "next" command block, which in most shows should
	 * contain a command to move to a different segment.
	 * <p>
	 * This is equivalent to the <code>segment_done</code> GRIN command.
	 */
	public void segmentDone() {
		if (segmentDoneCommand == null) {
			segmentDoneCommand = new SegmentDoneCommand(this);
		}
		runCommand(segmentDoneCommand);
	}

	/**
	 * Run the given command when we advance to the next frame. If the show has
	 * been destroyed, this has no effect.
	 * <p>
	 * It's OK to call this before the show has been initialized.
	 * 
	 * @param cmd
	 *            the command to run
	 */
	public void runCommand(Command cmd) {
		pendingCommands.add(cmd);
	}

	/**
	 * Run the given commands when we advance to the next frame. If the show has
	 * been destroyed, this has no effect.
	 * <p>
	 * It's OK to call this before the show has been initialized.
	 * 
	 * @param cmds
	 *            the commands to run
	 */
	public void runCommands(Command[] cmds) {
		if (cmds == null || cmds.length == 0) {
			return;
		}
		for (int i = 0; i < cmds.length; i++) {
			pendingCommands.add(cmds[i]);
		}
	}

	/**
	 * {@inheritDoc}
	 * <p>
	 * The animation framework calls this method just before calling
	 * {@link #nextFrame()} if the animation loop is caught up. From time to
	 * time, pending commands will be deferred until animation has caught up -
	 * this is done by the sync_display command. GRIN knows we've caught up when
	 * we paint a frame, but calling this method can let it know one frame
	 * earlier.
	 * 
	 * @see #nextFrame()
	 */
	public void setCaughtUp() {
		deferringPendingCommands = false;
	}

	/**
	 * {@inheritDoc}
	 * 
	 * @see #setCaughtUp()
	 */
	public void nextFrame() {
		if (currentSegment != null) {
			currentSegment.nextFrameForActiveFeatures(); // FIXME?
			currentSegment.nextFrameForRCHandlers();
		}
		director.notifyNextFrame();
	}

	/**
	 * Called from {@link Director#notifyNextFrame()}.
	 */
	void runPendingCommands() {
		while (!deferringPendingCommands && !pendingCommands.isEmpty()) {
			Command c = (Command) pendingCommands.remove();
			if (c != null) {
				c.execute(this); // Can call deferNextCommands()
			}
		}
	}

	/**
	 * This method, which should ONLY be called from the
	 * {@code Command#execute()} method of a command, suspends processing of
	 * further queued commands until the display of the show has caught up with
	 * the screen. This is done from the sync_display command, but can be done
	 * from any other command too. It causes an effect like
	 * {@code Toolkit.sync()} or the old X-Windows {@code XSync()} command,
	 * whereby the screen is guaranteed to be up-to-date before further commands
	 * are processed.
	 * <p>
	 * Doing this can be useful, for example, just before an operation that is
	 * time-consuming and CPU-bound on some browsers.
	 */
	public void deferNextCommands() {
		deferringPendingCommands = true;
	}

	/**
	 * This is called from {@link AnimationSegmentCommand}, and should not be
	 * called from anywhere else.
	 * 
	 * @param newSegment
	 *            the new segment to activate
	 */
	public void doActivateSegment(Segment newSegment) {
		Segment oldSegment = currentSegment;
		currentSegment = newSegment;
		currentSegment.activate(oldSegment);
		director.notifySegmentActivated(newSegment, oldSegment);
	}

	/**
	 * This is called from {@link SegmentDoneCommand}, and should not be called
	 * from anywhere else.
	 */
	public void doSegmentDone() {
		currentSegment.doSegmentDone();
	}

	/**
	 * Get the current segment.
	 * 
	 * @return the current segment
	 */
	public Segment getCurrentSegment() {
		return currentSegment;
	}

	/**
	 * {@inheritDoc}
	 * <p>
	 * Paint the current state of the enhancement.
	 */
	public void paintFrame(Graphics gc) {
		if (destroyed) {
			return;
		}
		if (currentSegment != null) {
			currentSegment.paintFrame(gc);
		}
		assert !deferringPendingCommands;
	}

	/**
	 * {@inheritDoc}
	 */
	public void paintDone() {
		if (destroyed) {
			return;
		}
		if (currentSegment != null) {
			currentSegment.paintDone();
		}
	}

	/**
	 * Called by the application when a key press is received.
	 * <p>
	 * A key press is queued and {@code true} is returned only if the current
	 * segment uses that key press. It is possible that the show will have a
	 * different current segment by the time the key press is processed, so
	 * there's some chance that a key press won't be queued, even if it is of
	 * interest to the state the show's about to be in. This should almost
	 * always be harmless, but if you wish to capture all key presses in your
	 * show, you can populate the needed segments with a {@code key_pressed}
	 * {@code rc_handler} with an empty body that captures all keys.
	 * 
	 * @param vkCode
	 * @return {@code true} if the key press is enqueued, and thus is expected
	 *         to be used
	 */
	public boolean handleKeyPressed(int vkCode) {
		if (currentSegment == null) {
			return false;
		}
		RCKeyEvent re = RCKeyEvent.getKeyByEventCode(vkCode);
		if (re == null) {
			return false;
		}
		if ((currentSegment.rcPressedInterest & re.getBitMask()) == 0) {
			return false;
		}
		pendingCommands.add(re);
		return true;
	}

	/**
	 * This is an implementation method, called by {@link RCKeyEvent}.
	 * 
	 * @param re
	 * @param caller
	 */
	public void internalHandleKeyPressed(RCKeyEvent re, Show caller) {
		if (currentSegment != null) {
			currentSegment.handleKeyPressed(re, caller);
		}
	}

	/**
	 * Called by the application when a key release is received. Note that not
	 * all devices generate a key released.
	 * <p>
	 * A key release is queued and true is returned only if the current segment
	 * uses that key release. It is possible that the show will have a different
	 * current segment by the time the key release is processed, so there's some
	 * chance that a key release won't be queued, even if it is of interest to
	 * the state the show's about to be in. This should almost always be
	 * harmless, but if you wish to capture all key releases in your show, you
	 * can populate the needed segments with a key_released rc_handler with an
	 * empty body that captures all keys.
	 * 
	 * @return true If the key release is enqueued, and thus is expected to be
	 *         used.
	 */
	public boolean handleKeyReleased(int vkCode) {
		if (currentSegment == null) {
			return false;
		}
		RCKeyEvent re = RCKeyEvent.getKeyByEventCode(vkCode);
		if (re == null) {
			return false;
		}
		if ((currentSegment.rcReleasedInterest & re.getBitMask()) == 0) {
			return false;
		}
		pendingCommands.add(re.getKeyReleased());
		return true;
	}

	/**
	 * This is an implementation method, called by {@link RCKeyEvent}.
	 * 
	 * @param re
	 * @param caller
	 */
	public void internalHandleKeyReleased(RCKeyEvent re, Show caller) {
		if (currentSegment != null) {
			currentSegment.handleKeyReleased(re, caller);
		}
	}

	/**
	 * Called by the application when the mouse moves. This should called when a
	 * mouse moved event or mouse dragged event is received.
	 * 
	 * @param x
	 * @param y
	 */
	public void handleMouseMoved(int x, int y) {
		boolean used = false;
		if (currentSegment != null) {
			used = currentSegment.handleMouse(x, y, false);
		}
		// TODO set cross hair cursor
	}

	public void handleMouseDragged(int x, int y) {
		// TODO
	}

	/**
	 * Called by the application when the mouse is clicked.
	 * 
	 * @param x
	 * @param y
	 */
	public void handleMouseClicked(int x, int y) {
		if (currentSegment != null) {
			currentSegment.handleMouse(x, y, true);
		}
		// TODO set cross hair cursor
	}

}
